Agile & People
General Web Development
PHP Core
Software Architecture
Test & Performance

Simplify WordPress Development with WP-CLI DevOps & Deployment

How to Automate WordPress Development Tasks with Powerful Command Line Tools

Milana Cap
Aug 12, 2024

What if I told you that you can generate a proper PHP code on a command? Remember, all I'm offering is the truth – nothing more. Setting up a project today is a project. GIT clone, nvm install, nvm use, npm install, npm run start, fix docker port issue, nvm install && nvm use, npm install, npm run start.. Ahh ok, rm -rf *, git clone… When you finally make all the different tools work on your machine, it’s time for writing the code - but not just yet. You still have to do some copy/pasting from docs. Where was that page with a code example again?

Two hours later, after you successfully reorganised your “code snippets” bookmark folder, you have the starter code pasted and now you can start coding. Now, just to quickly clean it up from the things you know you won’t need, maybe five minutes top…

Whew, it’s a good thing you Googled that weird part of code from the snippet because there were at least 3 Stack Overflow answers saying how you’re losing 3-5 milliseconds in performance with that legacy code. This is great: you’ve set up a new project with the latest code AND you’ve learned something new while doing so. All good to go to start coding first thing tomorrow.

You feel like you really accomplished something today, haven’t you? You’ve set up a project!

I hate that. I hate that something equivalent to opening a code editor and running up the gear takes so much time and effort that you feel accomplishment when, in fact, you haven’t even started.

But it doesn’t have to be that way. There is, in fact, a tool for that.

 

IPC NEWSLETTER

All news about PHP and web development

 

The Tool

It’s a wonderful, fast, powerful, open-source, CLI tool. The WP-CLI, command line interface for WordPress.

I’m a huge fan bjmof CLI tools. They do exactly what you tell them, and they even tell you what to tell them. They all work perfectly together even though they are not aware of each other. there is no “new look” or “this button has moved here”. They do exactly what they should, nothing more and nothing less.

As any other CLI tool, WP-CLI too has a global parameter –help with every command and subcommand, which will provide you with a complete documentation for said command or subcommand, altogether with usage examples. You’ll find things you need only once in a while, but there are also commands that are useful in your everyday WordPress development work.

I’m not going to run you through the installation process. It requires 3,5 commands and is pretty straightforward.

It’s worth noting here that WP-CLI is not officially supported on Windows machines. It is possible to install and use it on Windows machines, but some commands can have unexpected outcomes due to various reasons and maintainers don’t guarantee those can be fixed.

Everyday Work

There are different things you have to work on every day and there are only so many commands and flags you can memorise. Personally, I don’t like memorising anything. So, I use a lot of help and the global parameter –prompt. It will prompt you with every flag a command has so you don’t have to worry about typos or missing a required flag.

WP-CLI
Type values unique to your current use case

All you have to do is type values that are unique to your current use case. If you start using only this global parameter, you’re already 70% more productive than someone who doesn’t use it (I made up that stat).

There are many everyday tasks for which I use WP-CLI, too many to cover here. I’ll focus on those that require looking into docs to perform an informed copy/paste.

Install WordPress

My personal motto is “if you’re going to do it more than once, automate it”, regardless of if you’re doing it often or not.

If often, then you’ll be bored of typing the same thing. If rarely, you’ll always waste the same amount of time to figure out what exactly you need to do. Having a bash script requires only memorising that you have it for this specific task, and then using it.

The traditional way

Assuming you’ve already created new database and have a root folder for your install, installing WordPress contains 3 major steps:

  1. Download and unzip the latest version of WordPress
  2. Create config file with all database credentials
  3. Run the install script

Doing it in the traditional way, requires opening at least two different URLs in your browser, a few directories in your file manager, and one file in your code editor.

The WP-CLI way

Doing it in a terminal with WP-CLI, requires 3 commands:

  1. wp core download
  2. wp config create –dbuser=<DB_USER> –dbpass=<DB_PASSWORD> –dbname=<DB_NAME>
  3. wp core install –url= –title=

The lazy way

Automating it requires one command to run one bash script. I’ll create here a bash script for usage in your local environment, just to illustrate how far we can go with automating.

We’ll use those 3 commands from above. The first one doesn’t need any changes, unless, for some reason, you want an older version, in which case you can use –version flag.

wp core download

The second command needs some modification. First, in my local environment I’ll always have the same database user. With older mySQL versions it was root for me but with the latest update I had to change it to a different user. Regardless, it’s always the same and I can hardcode that value.

On the other hand, I never want to hardcode any important password, and I also don’t want any of those passwords being saved in bash history (which happens if you use WP-CLI –prompt). The solution is to use a silent prompt for bash command read.

The database name will be different for every use case so that one can also be handled by read command.

read -p 'Database name: ' dbname
read -sp 'Database password: ' dbpass
wp config create --dbuser=milana --dbpass=$dbpass --dbname=$dbname --prompt

 

These three are mandatory parameters and, most of the time, that’s enough for your install. However, if you need more custom settings, you can add –prompt for prompting all the other parameters.

The third command can be completely generated out of these two values we have. In my local environment, the URL is usually the same as the database name, with addition of .loc: –url=”${dbname}.loc”. This same URL can be used for admin user email address:

--_admin_email="admin@${dbname}.loc"_.```

Website title is not important here, especially if you are going to import a database from a remote website, but the title is a mandatory flag for this command so we can’t skip it. Using a database name will be sufficient: –title=$dbname.

For admin users I usually use admin for both username and password, because in the local environment I don’t really care about secure passwords for users. If you want it more secure for the same amount of work, you can use the database password, or you can put in a bit more effort and add another silent prompt for it.

Now we have something like this:

wp core install --url="${dbname}.loc" --title=$dbname --admin_user=admin --admin_password=admin --admin_email="admin@${dbname}.loc"

 

The script

Putting it all together in a script, we get just a handful of lines that you’ll never have to type again.

Create a new file and call it however you want. Remember, the file name will be a command you’ll run in the terminal. Mine is wp-install. You can add extension .sh but it’s not needed. Copy in the file our 3 commands preceded with Shebang:

#!/bin/bash
wp core download
read -p 'Database name: ' dbname
read -sp 'Database password: ' dbpass
wp config create --dbuser=milana --dbpass=$dbpass --dbname=$dbname --prompt
wp core install --url="http://${dbname}.loc" --title=$dbname --admin_user=admin --admin_password=admin --admin_email="admin@${dbname}.loc"

To run it we must make it executable:

chmod +x wp-install

And move it somewhere in our $PATH so that we can execute it in any directory we want. If you don’t know which directories are in your $PATH, you can run echo $PATH and you’ll see the list of all directories where you can send this file. I like to keep my scripts in

_/home/milana/bin_.

mv wp-install ~/bin/wp-install

Now you can run this script just by typing wp-install, and in a few seconds, you’ll have freshly installed WordPress in that directory.

WP Install
WP Install

Now, setting up this script might have taken a bit more time than you want to spend on installing local WordPress but guess what, that task will last only a few seconds from now on.

The beauty of this is that you can extend this script the exact way you want. You can add plugins or themes you frequently use, set specific language, generate dummy content, or you can make it a part of other scripts. The possibilities are endless.

 

EVERYTHING IS CONNECTED TO THE INTERNET

Explore the Web Development Track

 

Scaffold plugin

Now that we have WordPress installed, it’s time to write some code. Before you head off to docs to find that copy/paste code for your plugin or theme, look at help under scaffold command. Chances are you’ll find a command for what you need.

A completely new plugin is just one command away:

wp scaffold plugin --prompt

Scaffold plugin
Scaffold plugin

Parameters for scaffolding the plugin:

  • Slug (mandatory): The internal name of the plugin. Also, the directory name if not overridden by the next argument.
  • Directory name: Put the new plugin in some arbitrary directory path. Plugin directory will be path plus supplied slug.
  • Plugin name: What to put in the ‘Plugin Name:’ header.
  • Plugin description: What to put in the ‘Description:’ header.
  • Plugin author: What to put in the ‘Author:’ header.
  • Plugin author URI: What to put in the ‘Author URI:’ header.
  • Plugin URI: What to put in the ‘Plugin URI:’ header.
  • Skip tests: Don’t generate files for unit testing.
  • CI: Choose a configuration file for a continuous integration provider. Default is Circle.
  • Activate: Activate the newly generated plugin.
  • Activate network: Activate the newly generated plugin for the whole network (if it is multisite).
  • Force: Overwrite files that already exist.

This will not only create a new plugin in your plugins directory, but it will also generate everything you need to start writing your custom code, tests, use npm, and generate translation files.

Custom Code
Everything to start writing your custom code

On top of that, readme.txt file is the readme template used by wordpress.org, so if you want to host your plugin at WordPress.org plugin repository, you’re all set up. All you have to add now is what’s unique to your plugin.

Scaffold CPT

OK, so now can we start writing code? Well, not quite. I want a custom post type (CPT) in this plugin. This is yet another code snippet you would copy/paste from official docs. And yet, it’s right there, available in your terminal with, you guessed it, only one command.

wp scaffold post-type –prompt

Scaffold CPT
Parameters for scaffolding the post type

 

Parameters for scaffolding the post type:

  • Slug (mandatory): The internal name of the post type.
  • Label: Name of the post type shown in the menu. Usually plural.
  • Textdomain: The textdomain to use for the labels. If not specified, it will take the plugins or themes it’s the part of.
  • Dashicon: The dashicon to use in the menu. Default is admin-post.
  • Theme: Using –theme will add CPT in the active theme; defining a theme slug will place CPT in the provided theme.
  • Plugin: Create a file in the given plugin’s directory. If no theme or plugin is defined, the code will be sent to STDOUT.
  • Raw: Just generate the register_post_type() call and nothing else.
  • Force: Overwrite files that already exist.

Now we have a new directory inside our plugin.

New directory New directory inside our plugin

And to make it work, we need to write the code now. First, we want to prevent direct access to the plugin file and, after that, we will include the file with our CPT code.

// Your code starts here.
if ( ! defined( 'ABSPATH' ) ) {
die();
}
include_once __DIR__ . '/post-types/developers.php';

In only 4 lines of code that we actually wrote, we have a ready to use, fully functional plugin with custom post type.

Rare occasions

Coding in production, anyone? Nobody’s doing it anymore, and for a good reason. In fact, today’s tools and accepted workflows are made to keep you as far away from production as possible and I support that. Really, that’s the smartest thing to do.

However…There are some rare occasions when something has to be done only once and only in production.

Let’s say you need data from an external API to be imported in production. You will do this only once and, after that, the system will just use that data.

Or let’s go further, let’s consider the idea when you must do something that has nothing to do with the project itself. For example, a client asks you to provide them with a list of all users on the website with the company’s email address (because they want a list of staff who have access to the website). You don’t want to do this manually, why would you? Or to spend hours on coding this functionality and going through the whole workflow of GIT pull request and code review, and deployment build. You don’t want this code inside your project’s code base because it has nothing to do with it.

These along with many other reasons, are all valid to do something directly in production, and only once.

The query

The thing I love about WP-CLI is that it can create a very specific query in one command. Doing it with PHP can take 10+ lines of code.

Returning the results of this query inside the loop with PHP can take another 5-10 lines of code, while with WP-CLI we are still in one-liner.

Exporting these results into a file takes, you guessed it, another 5-10 lines of PHP code, while with WP-CLI, you guessed it again, it’s still one line. In fact, with WP-CLI changing the output format is just one parameter. With PHP it’s a bit more complicated.

If I want to list all users and return only their username, display name and email address with WP-CLI, I’d use this command:

wp user list --fields=user_login,user_email,display_name

List all users
List all users and return only username, display name, and email address

The PHP code for the same result is this:

$users = get_users(
[
'fields' => [
'user_login',
'user_email',
'display_name'
],
]
);

PHP Code
PHP Code

If I want to export this list into CSV file with WP-CLI, all I have to do is to point at a file:

wp user list --fields=user_login,user_email,display_name > users.csv

If the file doesn’t exist, one will be created.

This is what it takes with PHP:

$users = get_users(
[
'fields' => [
'user_login',
'user_email',
'display_name'
],
]
);
$file = fopen( 'users.csv', 'w' );
foreach ( $users as $user ) {
fputcsv( $file, (array) $user );
}
fclose( $file );

The rare query

Now that we have established how much faster it is to use WP-CLI for our rare occasion, let’s go back to that unrelated client’s request to export the list of all staff users – users with website’s domain name in their email address.

For this, we’re going to include the search parameters in our query. We want to search for wpcli.loc inside the user_email column.

In PHP that would look like this:

$users = get_users(
[
'fields' => [
'user_login',
'user_email',
'display_name'
],
'search' => '*wpcli.loc',
'search_columns' => [
'user_email'
],
]
);

NOTE: You might have noticed by now, but in case you didn’t – WP-CLI uses the same exact parameters as the PHP part of WordPress so you can easily convert one into another.

With WP-CLI it’s still one liner:

wp user list --fields=user_login,user_email,display_name --search=*wpcli.loc --search-columns=user_email

But the problem is it doesn’t return the desired result. It completely ignores the –search-columns=user_email parameter.

WP CLI Search results
Search results

As you can see in the screenshot above, it returns the user that has no wpcli.loc email but has it in the user_url column. Obviously, in this case you can change the search query to *@wpcli.loc and it will give you desired results, but you will never be sure that results are accurate because there are other fields in which this search keyword can appear, one of them being user description.

Combine what’s working

One way to overcome this and still use the fast way for completing the task, is to combine WP-CLI and PHP, using the things that work in each. We’ll use the query from PHP and execution from WP-CLI.

Create a new PHP file in WordPress root.

touch get-users.php

And place our PHP query inside it.

$users = get_users(
[
'fields' => [
'user_login',
'user_email',
'display_name'
],
'search' => '*wpcli.loc',
'search_columns' => [
'user_email'
],
]
);
$file = fopen( 'users.csv', 'w' );
foreach ( $users as $user ) {
fputcsv( $file, (array) $user );
}
fclose( $file );

To execute it, we’ll use WP-CLI:

wp eval-file get-users.php

Create a new PHP file in WordPress root
Create a new PHP file in WordPress root

And it’s done! That’s all it takes. The last thing I don’t like is the lack of feedback. I can’t see if it’s completed or anything that’s happening. Luckily, WP-CLI has an internal API that can help with it.

CLI feedback

First, I want to know that users are being processed. This can be done with WP_CLI::log.

foreach ( $users as $user ) {
WP_CLI::log( sprintf( 'Reading user: %s.', $user->display_name ) );
fputcsv( $file, (array) $user );
}

Then I want to know if the file has been created.

$filename = 'users.csv';
$file = fopen( $filename, 'w' );
foreach ( $users as $user ) {
WP_CLI::log( sprintf( 'Reading user: %s.', $user->display_name ) );
fputcsv( $file, (array) $user );
}
fclose( $file );
if ( ! file_exists( $filename ) ) {
WP_CLI::error( sprintf( 'Failed to create %s file.', $filename ) );
} else {
WP_CLI::success( sprintf( 'Created %s file.', $filename ) );
}

And finally, I want to see a nice table of all the exported users. For that I’m going to use WP_CLI\Utils\format_items(), and to make it work I’ll change a few things in existing code.

First parameter is $format and for that I want to use a table.

WP_CLI\Utils\format_items( 'table', $items, $fields );

Second parameter is $items, an array of arrays where each array is one user.

$items = [];
foreach ( $users as $user ) {
WP_CLI::log( sprintf( 'Reading user: %s.', $user->display_name ) );
fputcsv( $file, (array) $user );
$items[] = (array) $user;
}

And the last parameter, $fields, is an array of table headings for which we can use the same things WP-CLI does – field names (or database columns names).

$fields = [
'user_login',
'user_email',
'display_name'
];
$users = get_users(
[
'fields' => $fields,
'search' => '*wpcli.loc',
'search_columns' => [
'user_email'
],
]
);

The whole file now looks like this:

<?php
/**
* Get all users with `wpcli.loc` in their email address
* and export them to `users.csv` file.
*
* Execute this script with:
* wp eval-file get-users.php
*/
$fields = [
'user_login',
'user_email',
'display_name'
];
$users = get_users(
[
'fields' => $fields,
'search' => '*wpcli.loc',
'search_columns' => [
'user_email'
],
]
);
$filename = 'users.csv';
$file = fopen( $filename, 'w' );
$items = [];
foreach ( $users as $user ) {
WP_CLI::log( sprintf( 'Reading user: %s.', $user->display_name ) );
fputcsv( $file, (array) $user );
$items[] = (array) $user;
}
fclose( $file );
if ( ! file_exists( $filename ) ) {
WP_CLI::error( sprintf( 'Failed to create %s file.', $filename ) );
} else {
WP_CLI::success( sprintf( 'Created %s file. Following users have been exported:', $filename ) );
}
WP_CLI\Utils\format_items( 'table', $items, $fields );

Now we have a useful script with feedback about progress and status.

Useful script with feedback about progress and status.t
A useful script with feedback about progress and status.

You can go further and make it more refined, cover more error cases, even run WP-CLI commands from this script.

You can also easily import users from this file into another WordPress website (or the same if needed) with wp user import-csv command.

 

IPC NEWSLETTER

All news about PHP and web development

 

Conclusion

Whether you’re a seasoned WordPress developer or you get occasional WordPress projects every once in a while, WP-CLI is a tool worth looking at and trying out. It will provide you with everything you need to start coding that custom feature, including testing suite, task runner, translation files and all of that by following recommended best practices and WordPress coding standards.

Combining it with other CLI tools and PHP makes it even more powerful in a way that it can perform tedious and complex tasks with such precision and speed, you will wonder how you could ever live without it. Development before WP-CLI? I don’t even remember it.

Top Articles About DevOps & Deployment

Stay tuned!

Register for our newsletter

Behind the Tracks of IPC

PHP Core
Best practices & applications

General Web Development
Broader web development topics

Test & Performance
Software testing and performance improvements

Agile & People
Getting agile right is so important

Software Architecture
All about PHP frameworks, concepts &
environments

DevOps & Deployment
Learn about DevOps and transform your development pipeline

Content Management Systems
Sessions on content management systems

#slideless (pure coding)
See how technology really works

Web Security
All about
web security

PUSH YOUR CODE FURTHER